package p.ithorns.framework.common.time;


import javax.validation.constraints.NotNull;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.WeekFields;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * TimeUtil
 * <p>
 * 时间工具
 *
 * @author ithorns
 * @version 1.0
 * @date 2020/4/15 20:10
 */
public class TimeUtil {

    public final static ZoneId UTC = ZoneId.of("UTC");

    public final static ZoneId LOCAL_ZONE_ID = ZoneId.systemDefault();

    /**
     * 初始时间
     */
    public static final String ORIGIN = "1970-01-01 00:00:00.000";

    /**
     * 初始时间戳
     */
    public static final long ORIGIN_TIMESTAMP = 0L;

    /**
     * 日期的正则
     */
    public static final String REGEX_DATE =
            "^(([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})-(((0[13578]|1[02])-" +
                    "(0[1-9]|[12][0-9]|3[01]))|((0[469]|11)-(0[1-9]|[12][0-9]|30))|(02-(0[1-9]|[1][0-9]|2[0-8]))))" +
                    "|((([0-9]{2})(0[48]|[2468][048]|[13579][26])|((0[48]|[2468][048]|[13579][26])00))-02-29)$";

    /**
     * 正则: 年-周(yyyy-ww)
     */
    public static final String REGEX_YEAR_WEEK = "^((2[0-9]{3})|(19[0-9]{2}))-((0[1-9])|([1-4][0-9])|(5[0-3]))$";

    /**
     * 正则: 年-月(yyyy-MM)
     */
    public static final String REGEX_YEAR_MONTH = "^((2[0-9]{3})|(19[0-9]{2}))-([01][0-2])$";

    /**
     * 默认日期时间格式
     */
    public final static String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd[ HH[:mm][:ss][.SSS]]";
    /**
     * 默认日期格式
     */
    public final static String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    /**
     * 默认时间格式
     */
    public final static String DEFAULT_TIME_FORMAT = "HH[:mm][:ss][.SSS]";

    /**
     * 分钟时间格式
     */
    public final static String DEFAULT_MINUTE_TIME_FORMAT = "HH:mm";

    /**
     * 默认星期类型
     * WeekFields.of(DayOfWeek.MONDAY, 3);
     * <p>
     * WeekFields.ISO 对应Mysql的Week-Mode-3
     */
    public final static WeekFields LOCAL_WEEK_FIELDS = WeekFields.ISO;

    private final static int SECONDS = 60;

    private final static DateTimeFormatter DATE_TIME_FORMATTER
            = DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT);

    private final static ZoneOffset LOCAL_ZONE_OFFSET = OffsetDateTime.now().getOffset();


    public enum Type {
        /**
         * time period
         */
        DAY, HOUR, MINUTE, SECOND, MILLI, NANO;
    }

    public static String now() {
        return Instant
                .now()
                .atZone(ZoneId.systemDefault())
                .format(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT));
    }

    /**
     * 获取当前日期事件Date
     *
     * @return Date
     */
    public static Date nowDate() {
        return Date.from(Instant.now());
    }

    public static Date toDate(long timestamp) {
        return new Date(timestamp);
    }


    /**
     * 获取当前时间戳
     */
    public static Long timestamp() {
        return Instant.now().toEpochMilli();
    }

    /**
     * LocalDateTime to java.util.Date
     *
     * @param dateTime - LocalDateTime
     * @return java.util.Date
     */
    public static Date toDate(@NotNull LocalDateTime dateTime) {
        return Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
    }

    /**
     * LocalDate => Date
     *
     * @param localDate -
     * @return Date
     */
    public static Date toDate(@NotNull LocalDate localDate) {
        return Date.from(localDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
    }


    public static ZonedDateTime asZonedDateTime(@NotNull Date date) {
        return Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault());
    }

    public static LocalTime asLocalTime(@NotNull Date date) {
        return asZonedDateTime(date).toLocalTime();
    }

    /**
     * Date -> LocalDate
     *
     * @param date -
     * @return LocalDate
     */
    public static LocalDate asLocalDate(@NotNull Date date) {
        return asZonedDateTime(date).toLocalDate();
    }

    /**
     * Date -> LocalDateTime
     *
     * @param date -
     * @return LocalDateTime
     */
    public static LocalDateTime asLocalDateTime(@NotNull Date date) {
        return asZonedDateTime(date).toLocalDateTime();
    }

    /**
     * 日期/时间转字符串
     *
     * @param format   -日期/时间格式
     * @param temporal -需要转换的日期/时间
     * @return String
     */
    public static String toString(@NotNull Temporal temporal, @NotNull String format) {
        return DateTimeFormatter.ofPattern(format).format(temporal);
    }

    /**
     * 默认格式转换字符串
     * 支持格式：yyyy-MM-dd[ HH:mm[:ss][:SSS]]
     *
     * @param temporal -需要转换的日期/时间
     * @return String
     */
    public static String toString(@NotNull Temporal temporal) {
        return DATE_TIME_FORMATTER.format(temporal);
    }

    /**
     * Date 转换成LocalDateTime后格式化
     *
     * @param date   -
     * @param format -
     * @return String
     */
    public static String toString(@NotNull Date date, @NotNull String format) {
        return toString(asLocalDateTime(date), format);
    }

    /**
     * 格式化毫秒时间戳到时间字符串
     *
     * @param timestamp -时间戳
     * @return String
     */
    public static String toString(long timestamp) {
        return toString(timestamp, DEFAULT_DATE_TIME_FORMAT);
    }

    /**
     * 格式化毫秒时间戳到时间字符串
     *
     * @param timestamp -时间戳
     * @param format    -格式
     * @return String
     */
    public static String toString(long timestamp, @NotNull String format) {
        LocalDateTime time = asLocalDateTime(timestamp);
        return toString(time, format);
    }

    /**
     * localDateTime 转毫秒数
     *
     * @param localDateTime -待转时间
     * @return long 时间戳
     */
    public static long toMillis(LocalDateTime localDateTime) {
        return toMillis(localDateTime, LOCAL_ZONE_ID);
    }

    private static long toMillis(LocalDateTime localDateTime, ZoneId zoneId) {
        return localDateTime.atZone(zoneId).toInstant().toEpochMilli();
    }

    /**
     * LocalDate 转时间戳
     *
     * @param localDate -待转日期
     * @return long 时间戳
     */
    public static long toMillis(LocalDate localDate) {
        return toMillis(localDate, LOCAL_ZONE_ID);
    }

    private static long toMillis(LocalDate localDate, ZoneId zoneId) {
        return localDate.atStartOfDay(zoneId).toInstant().toEpochMilli();
    }

    public static long toMillis(String dateTime, ZoneId zoneId) {
        LocalDateTime parse = LocalDateTime.parse(dateTime, DATE_TIME_FORMATTER);
        return toMillis(parse, zoneId);
    }

    /**
     * 字符串转ZonedDateTime
     *
     * @param dateStr -字符串
     * @param format  -格式
     * @return ZonedDateTime
     */
    public static ZonedDateTime asZonedDateTime(@NotNull String dateStr, @NotNull String format) {
        return ZonedDateTime.parse(dateStr, DateTimeFormatter.ofPattern(format).withZone(LOCAL_ZONE_ID));
    }

    /**
     * 字符串格式化,字符串必须符合 yyyy-MM-dd 格式,当字符串非法时,例如 201s-00-35 时,返回 null
     *
     * @param dateStr 将要 LocalDate 化的字符串
     * @return LocalDate
     */
    public static LocalDate asLocalDate(@NotNull String dateStr, @NotNull String format) {
        return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern(format).withZone(LOCAL_ZONE_ID));
    }

    public static LocalDate asLocalDate(String date) {
        return asLocalDate(date, DEFAULT_DATE_FORMAT);
    }

    public static LocalTime asLocalTime(@NotNull String timeStr, @NotNull String format) {
        return LocalTime.parse(timeStr, DateTimeFormatter.ofPattern(format).withZone(LOCAL_ZONE_ID));
    }

    public static LocalDateTime asLocalDateTime(@NotNull String dateTimeStr) {
        return LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)
                .withZone(LOCAL_ZONE_ID));
    }

    public static LocalDateTime asLocalDateTime(@NotNull String dateTimeStr, @NotNull String format) {
        return LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ofPattern(format).withZone(LOCAL_ZONE_ID));
    }

    /**
     * 毫秒数 转 localDateTime
     *
     * @param timestamp -待转毫秒数
     * @return LocalDateTime 转后时间
     */
    public static LocalDateTime asLocalDateTime(long timestamp) {
        return LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), LOCAL_ZONE_ID);
        //return LocalDateTime.ofEpochSecond(timestamp / 1000, 0, OffsetDateTime.now(LOCAL_ZONE_ID).getOffset());
    }

    /**
     * 获取两个日期时间差
     *
     * @param start  -开始日期/时间
     * @param end    -结束日期/时间
     * @param format -格式
     * @return Period
     */
    public static Period getPeriod(String start, String end, String format) {
        LocalDate startDate = asLocalDate(start, format);
        LocalDate endDate = asLocalDate(end, format);
        return Period.between(startDate, endDate);
    }

    /**
     * 日期/时间字符串转时间戳
     *
     * @param dateTime -日期/时间
     * @return long
     */
    public static long toMillis(String dateTime) {
        LocalDateTime parse = LocalDateTime.parse(dateTime, DATE_TIME_FORMATTER);
        return toMillis(parse);
    }

    /**
     * 获取时间间隔 (最大到day)
     *
     * @param start -开始时间
     * @param end   -结束时间
     * @param type  -间隔类型 {@link Type}
     * @return long
     */
    public static long getDuration(LocalDateTime start, LocalDateTime end, TimeUtil.Type type) {
        Duration duration = Duration.between(start, end);
        switch (type) {
            case DAY:
                return duration.toDays();
            case HOUR:
                return duration.toHours();
            case MINUTE:
                return duration.toMinutes();
            case SECOND:
                return duration.toMillis();
            default:
                return duration.toNanos();
        }
    }

    /**
     * 获取过去几年的LocalDate
     *
     * @param num -过去的年数
     * @return LocalDate
     */
    public static LocalDate getPastYear(int num) {
        return LocalDate.now().minusYears(num);
    }

    /**
     * 获取去年的年份字符串
     *
     * @return String
     */
    public static String getLastYear() {
        return toString(getPastYear(1), "yyyy");
    }

    /**
     * 获取指定日期的一年中的周数
     *
     * @param date - 日期
     * @return int - 周数
     */
    public static int getWeekOfYear(LocalDate date) {
        return date.get(LOCAL_WEEK_FIELDS.weekOfWeekBasedYear());
    }

    /**
     * 获取年-周
     *
     * @param date -日期
     * @return String - yyyyww
     */
    public static String getYearWeek(LocalDate date) {
        int year = date.get(LOCAL_WEEK_FIELDS.weekBasedYear());
        int week = date.get(LOCAL_WEEK_FIELDS.weekOfWeekBasedYear());
        return year + "" + (week < 10 ? "0" + week : week);
    }

    /**
     * 校验日期/时间字符串是否合法格式
     *
     * @param fmt      -
     * @param datetime -
     * @return boolean
     */
    public static boolean valid(String fmt, String datetime) {
        DateTimeFormatter formatter = DateTimeFormatter
                .ofPattern(fmt.replace("y", "u"))
                .withResolverStyle(ResolverStyle.STRICT);
        try {
            formatter.parse(datetime);
        } catch (DateTimeParseException | NullPointerException e) {
            System.out.println(e.getMessage());
            return false;
        }
        return true;
    }

    /**
     * 将时间字符串按指定时间间隔向上取整
     *
     * @param timeStr  -时间字符串
     * @param interval -间隔
     * @return LocalTime
     */
    public static LocalTime roundUp(String timeStr, int interval) {
        return roundUp(TimeUtil.asLocalTime(timeStr, DEFAULT_DATE_TIME_FORMAT), interval);
    }

    /**
     * 将时间字符串按指定时间间隔向下取整
     *
     * @param timeStr  -时间字符串
     * @param interval -间隔
     * @return LocalTime
     */
    public static LocalTime roundDown(String timeStr, int interval) {
        return roundDown(TimeUtil.asLocalTime(timeStr, DEFAULT_DATE_TIME_FORMAT), interval);
    }

    /**
     * 指定时间间隔向上取整
     *
     * @param time     -时间
     * @param interval -间隔 单位:秒
     * @return LocalTime
     */
    public static LocalTime roundUp(LocalTime time, int interval) {
        int s1 = time.toSecondOfDay();
        int tmp = interval * SECONDS;
        if (s1 % tmp == 0) {
            return time;
        }
        int s2 = (s1 / tmp) * tmp;
        return LocalTime.ofSecondOfDay(s2);
    }

    /**
     * 指定时间间隔向下取整
     *
     * @param time     -时间
     * @param interval -间隔
     * @return LocalTime
     */
    public static LocalTime roundDown(LocalTime time, int interval) {
        int s1 = time.toSecondOfDay();
        int tmp = interval * SECONDS;
        if (s1 % tmp == 0) {
            return time;
        }
        // 86340 是23:59的分钟数 最大86440
        int s2 = Math.min(86340, (s1 / tmp + 1) * tmp);
        return LocalTime.ofSecondOfDay(s2);
    }

    /**
     * 根据周次及星期日获取日期
     *
     * @param year    -年
     * @param weeks   -周次
     * @param weekDay -星期日
     * @return LocalDate
     */
    public static LocalDate fromWeeks(int year, int weeks, int weekDay) {
        String w = weeks < 10 ? "0" + weeks : String.valueOf(weeks);
        return LocalDate.parse(year + "-W" + w + "-" + weekDay, DateTimeFormatter.ISO_WEEK_DATE);
    }

    /**
     * 年-周字符串获取指定星期日的日期
     *
     * @param yearWeek -年-周 yyyy-ww
     * @param weekDay  -星期日
     * @return LocalDate
     */
    public static LocalDate fromWeeks(@NotNull String yearWeek, int weekDay) {
        StringBuilder buf = new StringBuilder(yearWeek);
        int offset = buf.indexOf("-") + 1;
        buf.insert(offset, "W")
                .append("-")
                .append(weekDay);
        return LocalDate.parse(buf.toString(), DateTimeFormatter.ISO_WEEK_DATE);
    }

    /**
     * 获取本周结束的日期
     *
     * @param curDate -当前日期
     * @param weekEnd -周几为结束?
     * @return LocalDate
     */
    public static LocalDate getDateOfWeekEnd(LocalDate curDate, DayOfWeek weekEnd) {
        return curDate.with(TemporalAdjusters.nextOrSame(weekEnd));
    }

    /**
     * 获取本周开始的日期
     *
     * @param curDate   -当前日期
     * @param weedStart -周几为开始
     * @return LocalDate
     */
    public static LocalDate getDateOfWeekStart(LocalDate curDate, DayOfWeek weedStart) {
        return curDate.with(TemporalAdjusters.previousOrSame(weedStart));
    }

    /**
     * 获取某天开始时(00:00:00)的时间戳
     *
     * @param date -日期
     * @return 时间戳
     */
    public static long millisOfDayStart(LocalDate date) {
        return date.atStartOfDay(LOCAL_ZONE_ID).toInstant().toEpochMilli();
    }

    /**
     * 获取某天结束时(23:59:59)的时间戳
     *
     * @param date -日期
     * @return 时间戳
     */
    public static long millisOfDayEnd(LocalDate date) {
        return date.atTime(LocalTime.MAX).atZone(LOCAL_ZONE_ID).toInstant().toEpochMilli();
    }

    public static long millisOfHourStart(LocalDateTime time) {
        return time.withMinute(0).withSecond(0).withNano(0)
                .atZone(LOCAL_ZONE_ID).toInstant().toEpochMilli();
    }

    public static long millisOfHourEnd(LocalDateTime time) {
        return time.withMinute(59).withSecond(59)
                .atZone(LOCAL_ZONE_ID).toInstant().toEpochMilli();
    }

    /**
     * 是否今天
     *
     * @param dateStr yyyy-MM-dd
     * @return 结果
     */
    public static boolean isToday(String dateStr) {
        return LocalDate.now().equals(LocalDate.parse(dateStr));
    }

    /**
     * 是否本周
     *
     * @param weekStr yyyy-WW
     * @param weekDay 1~7
     * @return 结果
     */
    public static boolean isThisWeek(String weekStr, int weekDay) {
        return getWeekOfYear(fromWeeks(weekStr, weekDay)) == getWeekOfYear(LocalDate.now());
    }

    /**
     * 是否本月
     *
     * @param monthStr yyyy-MM
     * @return 结果
     */
    public static boolean isThisMonth(String monthStr) {
        return asLocalDate(String.format("%s-01", monthStr)).getMonthValue() == LocalDate.now().getMonthValue();
    }

    /**
     * 是否本年
     *
     * @param yearStr yyyy
     * @return 结果
     */
    public static boolean isThisYear(String yearStr) {
        return asLocalDate(String.format("%s-01-01", yearStr)).getYear() == LocalDate.now().getYear();
    }

    /**
     * 日期时间的时区转换
     *
     * @param dateTime -待转换的日期时间
     * @param from     -当前时区
     * @param to       -转换后的时区
     * @return LocalDateTime -转换后的日期时间
     */
    public static LocalDateTime convertTimeZone(LocalDateTime dateTime, ZoneId from, ZoneId to) {
        return dateTime.atZone(from).withZoneSameInstant(to).toLocalDateTime();
    }

    public static LocalDateTime convertTimeZone(long timestamp, ZoneId from, ZoneId to) {
        LocalDateTime dateTime = asLocalDateTime(timestamp);
        return convertTimeZone(dateTime, from, to);
    }

    /**
     * 日期的时区转换
     *
     * @param date -待转换的日期
     * @param from -当前时区
     * @param to   -转换后的时区
     * @return LocalDate -转换后的日期
     */
    public static LocalDate convertTimeZone(LocalDate date, ZoneId from, ZoneId to) {
        LocalTime time = LocalTime.of(0, 0);
        return LocalDateTime.of(date, time).atZone(from).withZoneSameInstant(to).toLocalDate();
    }

    /**
     * 时间的时区转换
     *
     * @param date -待转换的日期
     * @param time -待转换的时间
     * @param from -当前时区
     * @param to   -转换后的时区
     * @return LocalDate -转换后的时间
     */
    public static LocalTime convertTimeZone(LocalDate date, LocalTime time, ZoneId from, ZoneId to) {
        LocalDateTime of = LocalDateTime.of(date, time);
        return of.atZone(from).withZoneSameInstant(to).toLocalTime();
    }

    /**
     * 是否整点
     *
     * @param currentMillis -当前时间戳
     * @return boolean
     */
    public static boolean isHourPoint(long currentMillis) {
        //  在下一分钟到来之前都算
        return (currentMillis % 3600_000L) < 60_000;
    }

    /**
     * 判断指定时区的时间相对于系统时区是否是新的一天
     * <p>
     * 把目标时区的零点时间转成UTC， 看是不是跟系统时间一致，如果是一致的，说明当前目标是新一天
     *
     * @param srcZone       -源时区
     * @param currentMillis -时间
     * @return boolean
     */
    @SuppressWarnings("Duplicates")
    public static boolean isNewDayOfZone(ZoneId srcZone, long currentMillis) {
        LocalDateTime src = asLocalDateTime(currentMillis);
        // 转成目标时区
        LocalDateTime tgt = convertTimeZone(src, UTC, srcZone);
        // 如果时间比系统时间大，需要回拨1天
        LocalDateTime tmp = tgt.isAfter(src) ? src.plusDays(1) : src;

        // UTC零点时间
        LocalDateTime srcZero = tgt.with(LocalTime.MIN);

        int tgtDay = tgt.getDayOfYear();
        int srcDay = tmp.getDayOfYear();

        boolean isNewDay = false;
        if (tgtDay >= srcDay) {
            // 需要精确到分钟， 否则这一个小时里都会是相同的
            isNewDay = tgt.getHour() == srcZero.getHour()
                    && tgt.getMinute() == srcZero.getMinute();
        }
        // 当前时间小于并超过一天
        if (tmp.isBefore(tgt) && Duration.between(tmp, tgt).toDays() > 1) {
            throw new RuntimeException("The data time is earlier than the actual time for more than 1 day.");
        }
        return isNewDay;
    }

    /**
     * 检测时间戳是否是指定时区的零点
     *
     * @param source - 时间
     * @return Set<String> -目标集
     */
    public static List<String> hitZeroTimeWithZone(LocalDateTime source) {
        Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
        return availableZoneIds.stream()
                .filter(zone -> {
                    ZoneId targetZone = ZoneId.of(zone);
                    ZonedDateTime target = source.atZone(ZoneId.systemDefault()).withZoneSameInstant(targetZone);
                    return target.getHour() == 0;
                })
                .collect(Collectors.toList());
    }

    public static List<String> hitZeroTimeWithZone(long timestamp) {
        final LocalDateTime source = asLocalDateTime(timestamp);
        return hitZeroTimeWithZone(source);
    }


    public static void main(String[] args) {
        LocalDate date = LocalDate.of(2019, 11, 14);
        System.out.println(getWeekOfYear(date));
    }


    /**
     * @param s1
     * @param e1
     * @param s2
     * @param e2
     * @param isStrict 是否严格遵守不能重叠，例如如果为true 那么8：00-8：30 和8：30-9：00 时间段比较为true
     * @return true重叠。false不重叠
     */
    public static boolean isOverlap(LocalTime s1, LocalTime e1, LocalTime s2, LocalTime e2, boolean isStrict) {
        if (s1.isAfter(e1) || s2.isAfter(e2)) {
            throw new DateTimeException("endDate不能小于startDate");
        }
        if (isStrict) {
            if (!(s1.compareTo(e2) > 0 || e1.compareTo(s2) < 0)) {
                return true;
            }
        } else {
            if (!(s1.compareTo(e2) >= 0 || e1.compareTo(s2) <= 0)) {
                return true;
            }
        }
        return false;
    }
}
